<?php

namespace libraries;
!defined( 'APP_ROOT' ) && exit( 'Direct Access Deny!' );

use libraries\TimeUtils;
use enums\CachePrefixEnums;
use enums\ServiceEnums;

/**
 * 互斥锁
 * 
 * @author fzq
 * @comment 以Redis为基础实现的分布式锁 需要script的支持
 * @date 2016-09-02
 */
class DExclusiveLock //implements \Phalcon\DI\InjectionAwareInterface
{
	private $redis = null;
	private $_di = null;
	private static $_instance = null;
	
	private function __construct( $di ) 
	{
		$this->di = $di;
		$this->redis = $di[ ServiceEnums::SERVICE_REDIS_DB_LOCK ];
	}
	
	public static function getInstance( $di )
	{
		if( !self::$_instance )
		{
			self::$_instance = new DExclusiveLock( $di );
		}
		
		return self::$_instance;
	} 
		
	/**
	 * 加互斥锁（推荐此方法）
	 * 
	 * 算法：先查看是否持有锁 如果持有锁则直接返回否则加锁
	 * 
	 * 注：此方法在 ttl < $ttl && ttl > 0 时存在一个窗口 
	 * 解决此问题的方法即是在锁定以后对加的锁使用isHoldLock进行二次确认
	 * 
	 * @param string $strKey: biz key
	 * @param string $irecor: record id
	 * @param string $uid: user id
	 * @param int $ttl time to live
	 * @return boolean|number 
	 */
	public function lockEx( $strBiz, $iRecordID, $uid, $ttl = 600 )
	{
		if( !$this->redis )
			return false;
		
		$curTime = TimeUtils::getIntTime();
		$strKeyID = '"' . CachePrefixEnums::LOCK_EX_PREFIX . $strBiz . '_' . $iRecordID .  '"';
		
		if( is_string( $uid ) )
		{
			$uid = '"' . $uid . '"';
		}
		
$script = <<<EOT
local ttl = redis.call( 'ttl', $strKeyID );
if( ttl >= $ttl ) then
	
	local strVal = redis.call( 'get', $strKeyID );
	local objVal = cjson.decode( strVal );
	 
	if objVal['uid'] == $uid then
		return true;
	else
		return false;
	end
			
elseif ttl < 0 then
	
	local val = {};
	val["uid"] = $uid;
	val["localTime"] = $curTime;
	local setRes = redis.call( 'setnx', $strKeyID, cjson.encode( val ) );
	
	if setRes == 1 then
	
	return redis.call( 'expire', $strKeyID, $ttl );
			
	end
	
elseif ttl > 0 and ttl < $ttl then
	
	local strVal = redis.call( 'get', $strKeyID );
	local objVal = cjson.decode( strVal );
	 
	if objVal['uid'] == $uid then
			return redis.call( 'expire', $strKeyID, $ttl );
	else
		return false;
	end
			
else
			
	return false;
		
end
EOT;
		return $this->redis->eval( $script );
	}
	
	/**
	 * 加锁
	 * 
	 * @param string $strBiz
	 * @param int $id
	 * @param int $ttl
	 * @return boolean
	 */
	public function lock( $strBiz, $iRecordID, $ttl = 600 ) 
	{
		if( !$this->redis )
			return false;
		
		$strKeyID = CachePrefixEnums::LOCK_PREFIX . $strBiz . '_' . $iRecordID;
		
		if( $this->redis->set ( $strKeyID, TimeUtils::getIntTime(), [
				'NX',
				'EX' => $ttl
				] ))
		{
			return true;
		}

		return false;
	}

	/**
	 * 解扩展锁
	 * 
	 * 只能解自己持有的锁
	 * @param string $strBiz
	 * @param int $id
	 * @param int $uid
	 */
	public function unlockEx(  $strBiz, $iRecordID, $uid )
	{
		if( !$this->redis )
			return false;
		
		$strKeyID = '"' . CachePrefixEnums::LOCK_EX_PREFIX . $strBiz . '_' . $iRecordID .  '"';
		if( is_string( $uid ) )
		{
			$uid = '"' . $uid . '"';
		}
		
		$script = <<<EOT
local lockData = redis.call( 'get', $strKeyID );
		
if lockData == false then

return true;
		
else
		local objData = cjson.decode( lockData );

		if( objData[ 'uid' ] == $uid ) then
				return redis.call( 'del', $strKeyID );
		end

		return false;
end

EOT;
		return $this->redis->eval( $script );
	}
	
	/**
	 * 解锁
	 * @param string $strBiz
	 * @param int $iRecordID
	 */
	public function unlock( $strBiz, $iRecordID ) 
	{
		if( !$this->redis )
			return false;
		
		$strKeyID = CachePrefixEnums::LOCK_PREFIX . $strBiz . '_' . $iRecordID;
		
		return $this->redis->del ( $strKeyID );
	}
	
	/**
	 * 检测是否持有锁
	 * 
	 * 只对扩展锁有效
	 * @param $strBiz
	 * @param $id
	 * @param $uid
	 */
	public function isHoldLock( $strBiz, $iRecordID, $uid )
	{		
		if( !$this->redis )
			return false;
		
		if( is_string( $uid ) )
		{
			$uid = '"' . $uid . '"';
		}
		
		$strKeyID = '"' . CachePrefixEnums::LOCK_EX_PREFIX . $strBiz . '_' . $iRecordID .  '"';

		$script = <<<EOT
local lockData = redis.call( 'get', $strKeyID );
		
if lockData == false then

	return false;
		
else
	local objData = cjson.decode( lockData );

	if( objData[ 'uid' ] == $uid ) then
			return true;
	else
			return false;
	end
end

EOT;
		return $this->redis->eval( $script );
		
	}
	
	/**
	 * 互斥锁的持有者
	 * 
	 * @param string $strBiz
	 * @param int $iRecordID
	 * return object(stdClass)[97]
     * public 'localTime' => int 1473045341
     * public 'uid' => int 3
	 */
	public function getLockInfo( $strBiz, $iRecordID )
	{
		if( !$this->redis )
			return false;
		
		$strKeyID = CachePrefixEnums::LOCK_EX_PREFIX . $strBiz . '_' . $iRecordID;
		
		$strJson = $this->redis->get( $strKeyID );
		
		if( $strJson )
		{
			$jsonData = json_decode( $strJson );
			$jsonData->recordID = $iRecordID;
			$jsonData->biz = $strBiz;
			
			return $jsonData;
		}
		
		return false;
	}
// 	public function setDI(\Phalcon\DiInterface $dependencyInjector) 
// 	{
// 		$this->_di = $dependencyInjector;
// 	}

// 	public function getDI() 
// 	{
// 		return $this->_di;
// 	}

}